/**
 * Copyright (c) 2008 Trent Hoeppner
 * 
 * This file is part of BillCalc.
 * 
 * BillCalc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * BillCalc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with BillCalc.  If not, see <http://www.gnu.org/licenses/>.
 **/

package com.trentech.billcalc.gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import testcases.MainTest;

import com.trentech.billcalc.FinanceManager;
import com.trentech.billcalc.event.Event;
import com.trentech.billcalc.event.EventFactory;
import com.trentech.billcalc.event.InvalidResultException;

/**
 * EventEditor allows the user to view and change a single event.
 * 
 * @author Trent Hoeppner
 */
public class EventEditor extends JPanel {
    
    /**
     * Required for serialization.
     */
    private static final long serialVersionUID = -4571766316964934973L;
    
    
    /**
     * The format to display for editing purposes.
     */
    public static final DateFormat FORMAT = DateFormat.getDateInstance(DateFormat.LONG);

    
    /**
     * The formatter for money.
     */
    private static final NumberFormat CURRENCY_FORMAT = NumberFormat.getCurrencyInstance();

    
    /**
     * The status of the event in this.
     */
    private static enum EditingStatus {
        
        /**
         * Indicates that the user is editing an existing event.
         */
        EDITING,
        
        
        /**
         * Indicates that the user is just viewing the current event.
         */
        VIEWING,
        
        
        /**
         * Indicates that the user is editing a new event.
         */
        CREATING
    }
    
    
    /**
     * The listener to save and cancel notifications from this.
     */
    private EventEditorListener listener = null;
    
    
    /**
     * The current event being edited.  May be null.
     */
    private Event event = null;
    
    
    /**
     * The Action used to save the properties from the editors to the current 
     * Event.
     */
    private SaveAction saveAction = null;


    /**
     * The Action used to cancel editing of the current Event.
     */
    private CancelAction cancelAction = null;
    
    
    /**
     * The panel where editors for properties of the current event are placed.
     */
    private DetailView detailPanel = null;
    
    
    /**
     * The message above all the fields which describes the current editing 
     * status or error message.
     */
    private JLabel topMessage;
    
    
    /**
     * The overall editing status of the current event.
     */
    private EditingStatus status = EditingStatus.VIEWING;


    /**
     * True if one of the properties being edited has an error, false otherwise.
     */
    private boolean hasError;
    
    
    /**
     * The DataEditors that are currently displayed for editing the current 
     * event. 
     */
    private final List<DataEditor<? extends JComponent, ?>> editors = 
        new ArrayList<DataEditor<? extends JComponent, ?>>();
    
    
    /**
     * The factory for generating DataEditors.
     */
    private DataEditorFactory dataEditorFactory = new DataEditorFactory();


    /**
     * The manager that is used to validate Event data.
     */
    private FinanceManager manager;


    /**
     * The EventFactory for the current event, which is used to create 
     * temporary Events for validation.
     */
    private EventFactory factory;


    /**
     * The manager for tooltips and label highlighting.
     */
    private UserFriendlinessManager ufManager;
    
    
    /**
     * Constructor for EventEditor.
     * 
     * @param   manager     the FinanceManager that is used to validate Event 
     *                      data.  Cannot be null.
     * @param   ufManager   the manager for tooltips and label highlighting.  
     *                      Cannot be null.
     */
    public EventEditor(FinanceManager manager, UserFriendlinessManager ufManager) {
        this.manager = manager;
        this.ufManager = ufManager;
        
        setLayout(new BorderLayout());
        
        topMessage = new JLabel("");
        
        detailPanel = new DetailView(false);
        detailPanel.setTopLabel(topMessage);
        add(detailPanel, BorderLayout.CENTER);
        
        JPanel buttonPanel = new JPanel();
        saveAction = new SaveAction();
        cancelAction = new CancelAction();
        JButton saveButton = new JButton(saveAction);
        JButton cancelButton = new JButton(cancelAction);
        ufManager.registerToolTip(saveButton, 
                Messages.getString("EventEditor.saveButton.description"));
        ufManager.registerToolTip(cancelButton, 
                Messages.getString("EventEditor.cancelButton.description"));
        buttonPanel.add(saveButton);
        buttonPanel.add(cancelButton);
        setStatus(EditingStatus.VIEWING, null);
        add(buttonPanel, BorderLayout.SOUTH);
    }
    
    
    /**
     * Sets the save and cancel buttons to be enabled or disabled.
     */
    private void setActionsEnabled() {
        if (status == EditingStatus.VIEWING) {
            saveAction.setEnabled(false);
            cancelAction.setEnabled(false);
        }
        else if (!hasError) {
            saveAction.setEnabled(true);
            cancelAction.setEnabled(true);
        }
        else {
            saveAction.setEnabled(false);
            cancelAction.setEnabled(true);
        }
    }
    
    
    /**
     * Sets the overall status for editing the current event and updates the
     * the topMessage text and buttons accordingly.
     * 
     * @param   newStatus       the status to set.  Cannot be null.
     * @param   errorMessage    an error message if an error exists with one of 
     *                          the properties, or null if none of the 
     *                          properties has an error.
     */
    private void setStatus(EditingStatus newStatus, String errorMessage) {
        status = newStatus;
        hasError = errorMessage != null;
        
        if (!hasError) {
            String title = "";
            if (event != null) {
                String className = event.getClass().getSimpleName();
                String eventNounKey = className + ".createEditEventName";
                String eventNoun = Messages.getString(eventNounKey);
                String titleKey = "EventEditor.eventTitle." + newStatus.toString();
                title = Messages.getCompoundString(titleKey, eventNoun);
            }
            
            topMessage.setText(title);
            topMessage.setForeground(Color.BLACK);
        }
        else {
            topMessage.setText(errorMessage);
            topMessage.setForeground(Color.RED);
        }
        
        setActionsEnabled();
    }
    
    
    /**
     * Sets the listener which receives notifications from this on save or 
     * cancel.
     * 
     * @param   listener    the listener to set.  May be null to have no 
     *                      listener.
     */
    void setListener(EventEditorListener listener) {
        this.listener = listener;
    }
    
    
    /**
     * Notifies the listener that the given Event was updated with properties 
     * set by the user.
     * 
     * @param   savedEvent  the Event that was saved.  Cannot be null.
     */
    private void fireSaved(Event savedEvent) {
        if (listener != null) {
            listener.saved(savedEvent);
        }
    }
    
    
    /**
     * Notifies the listener that editing of the given Event was cancelled.
     * 
     * @param   cancelledEvent  the Event that was not saved.  Cannot be null.
     */
    private void fireCancelled(Event cancelledEvent) {
        if (listener != null) {
            listener.editingCancelled(cancelledEvent);
        }
    }
    
    
    /**
     * Validates the data for a single property, and updates the topMessage 
     * text and Color for the property's label accordingly.
     * 
     * @param   dataEditor  the DataEditor which represents the property to 
     *                      validate.  Cannot be null.
     * @param   tempEvent   the temporary Event which has the current values of
     *                      all data fields in the editor.
     *                      
     * @return  true if the property's data is valid, false otherwise.
     */
    private boolean validateSingle(DataEditor<? extends JComponent, ?> dataEditor, 
            Event tempEvent) {
    	boolean valid = true;
        JLabel label = dataEditor.getLabel();

        try {
            tempEvent.validate(dataEditor.getPropertyName(), manager);
            ufManager.setError(label, false);
            ufManager.setChanged(label, dataEditor.isChanged());

            // reset the top label
            setStatus(status, null);
        }
        catch (InvalidResultException e) {
            ufManager.setError(label, true);
            setStatus(status, label.getText() + ": " + e.getErrorMessage());
            valid = false;
        }

        return valid;
    }

    
    /**
     * Validates the given property, and if valid, validates other properties
     * until all properties are validated or one fails.  The invalid property
     * will be highlighted and an error message will be shown to describe the 
     * problem.
     * 
     * @param   dataEditor  the DataEditor which represents the property to 
     *                      validate.  Cannot be null.
     */
    private void validateWhileEditing(DataEditor<? extends JComponent, ?> dataEditor) {
        Event tempEvent = factory.createNewEvent();
        for (DataEditor<? extends JComponent, ?> currentEditor : editors) {
            currentEditor.commit(tempEvent);
        }
        
        boolean valid = validateSingle(dataEditor, tempEvent);
        if (valid) {
            for (DataEditor<? extends JComponent, ?> currentEditor : editors) {
                if (!validateSingle(currentEditor, tempEvent)) {
                    break;
                }
            }
        }
    }
    
    
    /**
     * Validates data properties until all are validated, or one is found to
     * be invalid.  The invalid property will be highlighted and an error 
     * message will be shown to describe the problem.
     */
    private void validateAll() {
        Event tempEvent = factory.createNewEvent();
        for (DataEditor<? extends JComponent, ?> dataEditor : editors) {
            dataEditor.commit(tempEvent);
        }

        for (DataEditor<? extends JComponent, ?> dataEditor : editors) {
        	
            if (!validateSingle(dataEditor, tempEvent)) {
                break;
            }
        }
    }
    
    
    /**
     * Saves changes for the Event currently being edited.
     */
    public void saveEvent() {
        for (DataEditor<? extends JComponent, ?> dataEditor : editors) {
            dataEditor.commit();
        }
    }
    
    
    /**
     * Sets the current Event, and sets up editors for each property of the 
     * Event.
     * 
     * @param   newEvent        the event to set.  May be null.
     * @param   alreadyExists   true indicates that the event exists and is for
     *                          editing, false indicates that it has just been 
     *                          created.
     */
    public void setEvent(Event newEvent, boolean alreadyExists) {
        editors.clear();
        
        event = newEvent;
        if (newEvent != null) {
            factory = EventFactory.valueOf(newEvent.getClass().getSimpleName());
        }
        else {
            factory = null;
        }
        
        for (Component component : detailPanel.getDetailComponents()) {
            if (!(component instanceof JLabel)) {
                ufManager.unregisterEventComponent((JComponent) component);
            }
        }
        
        List<JLabel> labels = new ArrayList<JLabel>();
        List<JComponent> components = new ArrayList<JComponent>();

        if (newEvent == null) {
            detailPanel.setComponents(labels, components);
            setStatus(EditingStatus.VIEWING, null);
            return;
        }
        
        if (alreadyExists) {
            setStatus(EditingStatus.EDITING, null);
        }
        else {
            setStatus(EditingStatus.CREATING, null);
        }
        
        for (String propertyName : getPropertyOrder(newEvent)) {
            DataEditor<? extends JComponent, ?> editor = 
                dataEditorFactory.createDataEditor(newEvent, factory, propertyName);
            editors.add(editor);
            String propertyDisplayName = factory.getLocalizedName(propertyName);
            JLabel label = editor.getLabel();
            label.setText(propertyDisplayName);
            JComponent component = editor.createEditor();
            labels.add(label);
            components.add(component);
            
            ufManager.registerEventComponent(factory, propertyName, component, label);
        }
        
        detailPanel.setComponents(labels, components);
        
        if (newEvent.isGenerated()) {
            setStatus(EditingStatus.VIEWING, null);
        }
        
        validateAll();
    }
    
    
    /**
     * Returns the order that properties should exist in the GUI for the given 
     * Event when editing.
     * 
     * @param   newEvent    the Event to find the property order for.  Cannot 
     *                      be null.
     *                      
     * @return  the list of properties of the given Event, in the order that 
     *          they should appear.  Will not be null.
     */
    private List<String> getPropertyOrder(Event newEvent) {
        String propertyOrderKey = newEvent.getClass().getSimpleName() + ".propertyOrder";
        String propertyOrder = Messages.getString(propertyOrderKey);

        List<String> ordering = new ArrayList<String>();
        StringTokenizer tokenizer = new StringTokenizer(propertyOrder, ", ");
        while (tokenizer.hasMoreTokens()) {
            String property = tokenizer.nextToken();
            ordering.add(property);
        }

        return ordering;
    }
    
    
    /**
     * Formats the given integer as a currency, such that the given integer is 
     * multiplied by 100 before formatting.  This method should be used for all
     * display purposes for money.  This method is the inverse of
     * {@link #parseMoneyForDisplay(String)}.
     * 
     * @param   value   the value to convert to a String.  Cannot be null.
     * 
     * @return  the formatted value.  Will not be null.
     */
    public static String formatMoneyForDisplay(Integer value) {
        // original : 
    	// String text = CURRENCY_FORMAT.format(value.doubleValue() / 100);
    	// seed fault : 
    	String text = CURRENCY_FORMAT.format(value.doubleValue() / (MainTest.isOriginal  ? 100 : 110));
    	
        return text;
    }
    
    
    /**
     * Parses the given text which represents a currency amount to produce an 
     * Integer.  This method is the inverse of 
     * {@link #formatMoneyForDisplay(Integer)}.
     * 
     * @param   text    the text to parse.  Cannot be null and must be in the 
     *                  expected format.
     *                  
     * @return  the Integer value.  Will not be null.
     * 
     * @throws  ParseException  if the format of the number was incorrect.
     */
    public static Integer parseMoneyForDisplay(String text) throws ParseException {
        Number number = CURRENCY_FORMAT.parse(text);
        int intValue = (int) (number.doubleValue() * 100);
        Integer value = Integer.valueOf(intValue);
        return value;
    }


    /**
     * DataEditorFactory is a factory for generating DataEditors.
     */
    public class DataEditorFactory {
        
        /**
         * Constructor for DataEditorFactory.
         */
        public DataEditorFactory() {
            // do nothing
        }
        

        /**
         * Factory method for creating DataEditors.
         * 
         * @param   editingEvent    the event to create the DataEditor for.  
         *                          Cannot be null.
         * @param   eventFactory    the EventFactory to create an editor for.  
         *                          Cannot be null.
         * @param   propertyName    the name of a property for event.  Cannot 
         *                          be null or empty.
         *                          
         * @return  a new DataEditor for the given property.  Will not be null.
         */
        public DataEditor<? extends JComponent, ?> createDataEditor(
                Event editingEvent, EventFactory eventFactory, String propertyName) {
            PropertyDescriptor propertyDescriptor = getPropertyDescriptor(eventFactory,
                    propertyName);
            
            DataEditor<? extends JComponent, ?> editor = null;
            try {
                Class<?> propertyType = propertyDescriptor.getPropertyType();
                if (propertyType.equals(String.class)) {
                    editor = new StringDataEditor(editingEvent, propertyDescriptor);
                }
                else if (propertyType.equals(Integer.class) || propertyType.equals(int.class)) {
                    if (eventFactory.isMoneyProperty(propertyName)) {
                        editor = new MoneyDataEditor(editingEvent, propertyDescriptor);
                    }
                    else {
                        editor = new IntegerDataEditor(editingEvent, propertyDescriptor);
                    }
                }
                else if (propertyType.equals(Date.class)) {
                    editor = new DateDataEditor(editingEvent, propertyDescriptor);
                }
                else if (propertyType.equals(Boolean.class)) {
                    editor = new BooleanDataEditor(editingEvent, propertyDescriptor);
                }
                else if (propertyType.equals(boolean.class)) {
                    editor = new BooleanDataEditor(editingEvent, propertyDescriptor);
                }
            }
            catch (IntrospectionException e) {
                e.printStackTrace();
            }
            
            return editor;
        }


        /**
         * Inspects the given event to find the requested property descriptor.
         * 
         * @param   eventFactory    the EventFactory which represents the Event
         *                          to get the property of.  Cannot be null.
         * @param   propertyName    the name of the property to get the 
         *                          descriptor of.  Cannot be null or empty.
         *                          
         * @return  the desired PropertyDescriptor.  Will not be null.
         */
        private PropertyDescriptor getPropertyDescriptor(EventFactory eventFactory,
                String propertyName) {
            
            PropertyDescriptor found = null;
            BeanInfo beanInfo = eventFactory.getInfo();
            for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                if (propertyDescriptor.getName().equals(propertyName)) {
                    found = propertyDescriptor;
                    break;
                }
            }

            return found;
        }

    }


    /**
     * DataEditor implementations are help to manage the relationship
     * between a JavaBean's property and the component used to edit that
     * property.  For simplicity, the JComponent is assumed to have the value
     * being edited, which can then be {@link #commit committed} (or saved) to 
     * the bean as needed.  This design does not allow for multiple views of 
     * the bean.
     */
    private interface DataEditor<ComponentType extends JComponent, PropertyType> {
        
        /**
         * Creates an editor which whose initial value will be the value of the 
         * property in the bean.
         * 
         * @return  an editor that is initialized to the bean's property.  Will
         *          not be null.
         */
        public ComponentType createEditor();
        

        /**
         * Returns the name of the Event property that this represents.
         * 
         * @return  the name of the property.  Will not be null.
         */
        public String getPropertyName();
        
        
        /**
         * Returns the current value of the property from the editor.
         * 
         * @return  the value of the property in the editor.  May be null if
         *          the bean and editor allow for it.
         */
        public PropertyType getValue();
        
        
        /**
         * Sets the current value of the property in the editor.
         * 
         * @param   value   the value of the property to set in the editor.
         *                  May be null if the bean and editor allow for it.
         */
        public void setValue(PropertyType value);
        
        
        /**
         * Takes the value from the editor and sets it in the bean.
         */
        public void commit();
        
        
        /**
         * Takes the value from the editor and sets it in the given Event.
         * 
         * @param   tempEvent   the Event to set the value of the property in. 
         *                      Cannot be null.
         */
        public void commit(Event tempEvent);
        
        
        /**
         * Returns the label that describes the property.
         * 
         * @return  the label that describes the property.  Will not be null.
         */
        public JLabel getLabel();
        
        
        /**
         * Returns whether the user has manually changed the value of this 
         * in the editor since it was created.
         * 
         * @return  true if the user has changed the value, false otherwise.
         */
        public boolean isChanged();
    }
    
    
    /**
     * AbstractDataEditor is the base implemenation of DataEditor.
     *
     * @param   <ComponentType> the type of Component created to edit the 
     *                          property.
     * @param   <PropertyType>  the type of the property to be edited.
     */
    abstract private class AbstractDataEditor<ComponentType extends JComponent, PropertyType> 
            implements DataEditor<ComponentType, PropertyType> {
        
        /**
         * The label that describes the property.
         */
        private JLabel label;
        
        
        /**
         * The editor as it is instantiated.
         */
        private ComponentType editor;
        
        
        /**
         * The Event that holds the initial value for the editor, and which 
         * {@link #commit} will write to.
         */
        private Event event;
        
        
        /**
         * The property descriptor for the property.
         */
        private PropertyDescriptor propertyDescriptor;
        
        
        /**
         * The read method for the property.
         */
        private Method readMethod;
        
        
        /**
         * The write method for the property.
         */
        private Method writeMethod;
        
        
        /**
         * True indicates that the user changed the value in the editor, false 
         * indicates that the user has not changed the value.
         */
        private boolean changed = false;
        
        
        /**
         * Constructor for AbstractDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the Event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public AbstractDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            
            this.event = event;

            this.propertyDescriptor = propertyDescriptor;
            readMethod = propertyDescriptor.getReadMethod();
            writeMethod = propertyDescriptor.getWriteMethod();
            
            label = new JLabel();
        }
        
        
        /**
         * {@inheritDoc}
         */
        public String getPropertyName() {
            return propertyDescriptor.getName();
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void commit() {
            commit(getEvent());
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void commit(Event tempEvent) {
            Object value = getValue();
            try {
                getWriteMethod().invoke(tempEvent, value);
            }
            catch (IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }


        /**
         * Returns the editor that was already created.
         * 
         * @return  the editor.  Will be null if the editor was not created 
         *          yet.
         */
        ComponentType getEditor() {
            return editor;
        }


        /**
         * Sets the editor.  This method should be called after creating the 
         * editor component.
         * 
         * @param   editor  the editor that was just created.  Cannot be null.
         */
        void setEditor(ComponentType editor) {
            this.editor = editor;
        }


        /**
         * Returns the Event to be edited.
         * 
         * @return  the Event to be edited.  Will not be null.
         */
        Event getEvent() {
            return event;
        }


        /**
         * Returns the Method to get the property from the bean.
         * 
         * @return  the Method to get the property from the bean.  Will not be 
         *          null.
         */
        Method getReadMethod() {
            return readMethod;
        }


        /**
         * Returns the Method to set the property for the bean.
         * 
         * @return  the Method to set the property for the bean.  Will not be 
         *          null.
         */
        Method getWriteMethod() {
            return writeMethod;
        }
        
        
        /**
         * Returns the property descriptor for the property.
         * 
         * @return  the property descriptor for the property.  Will not be null.
         */
        PropertyDescriptor getDescriptor() {
            return propertyDescriptor;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public JLabel getLabel() {
            return label;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public boolean isChanged() {
            return changed;
        }
        
        
        /**
         * Sets whether the user has changed this property or not.
         * 
         * @param   changed true if the user changed this, false if the user 
         *                  has not changed this.
         */
        void setChanged(boolean changed) {
            this.changed = changed;
        }
    }
    
    
    /**
     * TextFieldDataEditor is the base class for DataEditors that uses a 
     * JTextField to edit a property for an Event beans.
     * 
     * @param   <PropertyType>  the type of the property to set.
     */
    abstract private class TextFieldDataEditor<PropertyType> 
        extends AbstractDataEditor<JTextField, PropertyType> {
        
        /**
         * Constructor for TextFieldDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public TextFieldDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }
        
        
        /**
         * Creates a JTextField to be used to edit the property.
         * 
         * @return  a JTextField to edit the property.  Will not be null.
         */
        @SuppressWarnings("unchecked")
        public JTextField createEditor() {
            JTextField field = new JTextField();
            try {
                PropertyType value = (PropertyType) getReadMethod().invoke(getEvent());
                field.setText(toString(value));
            }
            catch (IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            setEditor(field);
            
            TextFieldListener textFieldListener = new TextFieldListener(this);
            field.getDocument().addDocumentListener(textFieldListener);
            
            return field;
        }
        
        
        /**
         * Converts the data to a String.  This is used to set the initial 
         * value of the JTextField.
         * 
         * @param   data    the data from the JavaBean to convert.
         * 
         * @return  the String form of the data, which can be converted back to
         *          data at a later point.  May be null if appropriate.
         */
        abstract String toString(PropertyType data);

        
        /**
         * TextFieldListener listens for changes to a text field and requests
         * validation after each change.
         */
        private class TextFieldListener implements DocumentListener {
            
            /**
             * The DataEditor that should be validated on change.
             */
            private DataEditor<? extends JComponent, ?> editor;
            
            
            /**
             * Constructor for TextFieldListener.
             * 
             * @param   editor  the editor that should be validated on change. 
             *                  Cannot be null.
             */
            public TextFieldListener(DataEditor<? extends JComponent, ?> editor) {
                this.editor = editor;
            }
            

            /**
             * {@inheritDoc}
             */
            public void changedUpdate(DocumentEvent e) {
                // do nothing
            }


            /**
             * {@inheritDoc}
             */
            public void insertUpdate(DocumentEvent e) {
                setChanged(true);
                validateWhileEditing(editor);
            }


            /**
             * {@inheritDoc}
             */
            public void removeUpdate(DocumentEvent e) {
                setChanged(true);
                validateWhileEditing(editor);
            }
        }
    }
    
    
    /**
     * StringDataEditor is a DataEditor for String properties on Events.
     */
    private class StringDataEditor extends TextFieldDataEditor<String> {
        
        /**
         * Constructor for StringDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public StringDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }


        /**
         * {@inheritDoc}
         */
        public String getValue() {
            return getEditor().getText();
        }


        /**
         * {@inheritDoc}
         */
        public void setValue(String value) {
            getEditor().setText(value);
        }


        /**
         * {@inheritDoc}
         */
        String toString(String data) {
            return data;
        }
    }
    
    
    /**
     * IntegerDataEditor is a DataEditor for Integer or int properties on 
     * Events.
     */
    private class IntegerDataEditor extends TextFieldDataEditor<Integer> {
        
        /**
         * Constructor for IntegerDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public IntegerDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }


        /**
         * {@inheritDoc}
         */
        public Integer getValue() {
            Integer value;
            try {
                value = Integer.parseInt(getEditor().getText());
            }
            catch (NumberFormatException e) {
                value = Integer.valueOf(0);
            }
            
            return value;
        }

        
        /**
         * {@inheritDoc}
         */
        public void setValue(Integer value) {
            getEditor().setText(value.toString());
        }


        /**
         * {@inheritDoc}
         */
        String toString(Integer data) {
            return data.toString();
        }
    }
    
    
    /**
     * MoneyDataEditor is a DataEditor for Integer or int properties on 
     * Events which are interpreted as money.  Money values may have a decimal
     * in the editor with up to 2 decimal places.  The value is multiplied by 
     * 100 for the actual Integer to be used. 
     */
    private class MoneyDataEditor extends TextFieldDataEditor<Integer> {
        
        /**
         * Constructor for IntegerDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public MoneyDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }


        /**
         * Creates a JTextField to be used to edit the property.
         * 
         * @return  a JTextField to edit the property.  Will not be null.
         */
        @SuppressWarnings("unchecked")
        public JTextField createEditor() {
            JTextField textField = super.createEditor();
            textField.setHorizontalAlignment(JTextField.RIGHT);
            return textField;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public Integer getValue() {
            String text = getEditor().getText();
            try {
                Integer value = parseMoneyForDisplay(text);
                return value;
            }
            catch (NumberFormatException e) {
                return Integer.valueOf(0);
            }
            catch (ParseException e) {
                return Integer.valueOf(0);
            }
        }

        
        /**
         * {@inheritDoc}
         */
        public void setValue(Integer value) {
            String text = formatMoneyForDisplay(value);
            getEditor().setText(text);
        }


        /**
         * {@inheritDoc}
         */
        String toString(Integer data) {
            String text = formatMoneyForDisplay(data);
            return text;
        }
    }
    
    
    /**
     * DateDataEditor is a DataEditor for Date properties on Events.
     */
    private class DateDataEditor extends TextFieldDataEditor<Date> {
        
        /**
         * Constructor for DateDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public DateDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }


        /**
         * {@inheritDoc}
         */
        public Date getValue() {
            try {
                return FORMAT.parse(getEditor().getText());
            }
            catch (ParseException e) {
                return null;
            }
        }


        /**
         * {@inheritDoc}
         */
        public void setValue(Date value) {
            getEditor().setText(FORMAT.format(value));
        }


        /**
         * {@inheritDoc}
         */
        String toString(Date data) {
            return FORMAT.format(data);
        }
    }
    
    
    /**
     * BooleanDataEditor is a DataEditor for boolean properties on Events.
     */
    private class BooleanDataEditor extends AbstractDataEditor<JCheckBox, Boolean> {
        
        /**
         * Constructor for BooleanDataEditor.
         * 
         * @param   event               the Event to be edited.  Cannot be 
         *                              null.
         * @param   propertyDescriptor  the descriptor for the property to be 
         *                              edited on the event.  Cannot be null.
         *                              
         * @throws  IntrospectionException  if there is a problem extracting a 
         *                                  read Method or write Method for the
         *                                  property.
         */
        public BooleanDataEditor(Event event, PropertyDescriptor propertyDescriptor) 
            throws IntrospectionException {
            super(event, propertyDescriptor);
        }


        /**
         * Creates a JCheckBox to be used to edit the property.
         * 
         * @return  a JCheckBox to edit the property.  Will not be null.
         */
        @SuppressWarnings("unchecked")
        public JCheckBox createEditor() {
            JCheckBox checkBox = new JCheckBox();
            try {
                Boolean value = (Boolean) getReadMethod().invoke(getEvent());
                checkBox.setSelected(value.booleanValue());
            }
            catch (IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            setEditor(checkBox);
            
            CheckBoxListener checkBoxListener = new CheckBoxListener(this);
            checkBox.addActionListener(checkBoxListener);
            
            return checkBox;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public Boolean getValue() {
            return getEditor().isSelected();
        }


        /**
         * {@inheritDoc}
         */
        public void setValue(Boolean value) {
            getEditor().setSelected(value);
        }

        
        /**
         * CheckBoxListener listens for changes to check box and requests
         * validation after each change.
         */
        private class CheckBoxListener implements ActionListener {
            
            /**
             * The DataEditor that should be validated on change.
             */
            private DataEditor<? extends JComponent, ?> editor;
            
            
            /**
             * Constructor for CheckBoxListener.
             * 
             * @param   editor  the editor that should be validated on change. 
             *                  Cannot be null.
             */
            public CheckBoxListener(DataEditor<? extends JComponent, ?> editor) {
                this.editor = editor;
            }
            

            /**
             * {@inheritDoc}
             */
            public void actionPerformed(ActionEvent e) {
                setChanged(true);
                validateWhileEditing(editor);
            }
            
        }
    }
    
    
    /**
     * The Action that cancels editing of the current Event.
     */
    public class CancelAction extends SimpleAction {
        
        /**
         * Required for serialization.
         */
        private static final long serialVersionUID = 2946468937257694859L;


        /**
         * Constructor for CancelAction.
         */
        public CancelAction() {
            super(null, "x.gif");
        }


        /**
         * Disables save and cancel and notifies the listener.
         * 
         * @param   e   the event that triggered this method call.  Ignored.
         */
        public void actionPerformed(ActionEvent e) {
            setStatus(EditingStatus.VIEWING, null);
            fireCancelled(event);
        }

    }


    /**
     * The Action that saves data from the editors to the properties of the 
     * current Event.
     */
    private class SaveAction extends SimpleAction {

        /**
         * Required for serialization.
         */
        private static final long serialVersionUID = -3972487586734239124L;


        /**
         * Constructor for SaveAction.
         */
        public SaveAction() {
            super(null, "download_green.gif");
        }


        /**
         * Saves the properties to the Event object, disables save and cancel 
         * and notifies the listener.
         * 
         * @param   e   the event that triggered this method call.  Ignored.
         */
        public void actionPerformed(ActionEvent e) {
            saveEvent();
            setStatus(EditingStatus.VIEWING, null);
            fireSaved(event);
        }

    }
}


/**
 * EventEditorListener is a simple interface used to listen to save and cancel 
 * notifications from an EventEditor.
 */
interface EventEditorListener {
    
    /**
     * Notifies this that the given Event was saved in the EventEditor.
     * 
     * @param   event   the Event that was saved.  Cannot be null.
     */
    public void saved(Event event);
    
    
    /**
     * Notifies this that the given Event was cancelled from saving in the 
     * EventEditor.
     * 
     * @param   event   the Event that was not saved.  Cannot be null.
     */
    public void editingCancelled(Event event);
}
